/*
 * Copyright 2011-2012 Rivoli team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.rivoli.model.classhierarchy

import com.googlecode.rivoli.RivoliException
import com.googlecode.rivoli.model.ElementDescriptor
import com.googlecode.rivoli.model.support.ModelSupport

import java.lang.reflect.Modifier

class ClassHierarchyModel extends ModelSupport {
	void init(Map<String, ?> parameters) throws RivoliException {
		if (!parameters.containsKey(PACKAGE_NAME)) {
			throw new RivoliException("missing required parameter ${PACKAGE_NAME}")
		}

		addElementDescriptors(Class.from(parameters[PACKAGE_NAME]).findAll {Class<?> clazz ->
			!clazz.annotation && !clazz.enum && !clazz.interface && Modifier.isPublic(clazz.modifiers)
		})
	}

	private Map<Class<?>, Set<?>> extractRenderers(Set<Class<?>> classes) throws RivoliException {
		Map<Class<?>, Set<?>> renderers = [:].withDefault {[] as Set}

		Set<Class<?>> rendererClasses = [] as Set

		classes.each {Class<?> clazz ->
			RendererFor rendererForAnnotation = clazz.getAnnotation(RendererFor)

			if (rendererForAnnotation) {
				rendererClasses << clazz

				renderers[rendererForAnnotation.value()] << clazz.newInstance()
			}
		}

		classes.removeAll(rendererClasses)

		renderers
	}

	private void addElementDescriptors(Set<Class<?>> classes) throws RivoliException {
		Map<Class<?>, Set<?>> renderers = extractRenderers(classes)

		Map<Class<?>, ClassBasedElementDescriptor> elementDescriptors = createElementDescriptors(classes, renderers)

		Set<Class<?>> unrenderables = new HashSet(renderers.keySet())
		unrenderables.removeAll(elementDescriptors.keySet())

		if (unrenderables) {
			throw new RivoliException("cannot render elements ${unrenderables}")
		}

		setParents(elementDescriptors)
		setContainerForBasedOnAnnotation(elementDescriptors)

		elementDescriptors.values().each {addElementDescriptor(it)}
	}

	private Map<Class<?>, ClassBasedElementDescriptor> createElementDescriptors(Set<Class<?>> elementDescriptorClasses, Map<Class<?>, Set<?>> renderers) {
		elementDescriptorClasses.collectEntries {Class<?> elementDescriptorClass ->
			ClassBasedElementDescriptor elementDescriptor = new ClassBasedElementDescriptor(elementDescriptorClass, renderers[elementDescriptorClass])

			Set<String> parentProperties = ['metaClass'] as Set
			parentProperties += elementDescriptorClass.superclass.metaClass.properties*.name

			elementDescriptorClass.metaClass.properties.findAll {!parentProperties.contains(it.name) && (it.name != 'children')}.each {MetaProperty property ->
				elementDescriptor.addAttributeDescriptor(property.name.humanize(), property.type)
			}

			[elementDescriptorClass, elementDescriptor]
		}
	}

	private void setParents(Map<Class<?>, ClassBasedElementDescriptor> elementDescriptors) throws RivoliException {
		elementDescriptors.keySet().each {Class<?> elementDescriptorClass ->
			Class<?> superclass = elementDescriptorClass.superclass

			if (superclass != Object) {
				ElementDescriptor parent = elementDescriptors[superclass]

				if (parent) {
					elementDescriptors[elementDescriptorClass].parent = parent
				} else {
					throw new RivoliException("class '${superclass.name}' can't be extended by '${elementDescriptorClass.name}', since it lies outside the model")
				}
			}
		}
	}

	private void setContainerForBasedOnAnnotation(Map<Class<?>, ClassBasedElementDescriptor> elementDescriptors) throws RivoliException {
		elementDescriptors.keySet().each {Class<?> elementDescriptorClass ->
			Class<?> containedType = Utilities.findContainedType(elementDescriptorClass, false)

			if (containedType) {
				ElementDescriptor containerFor = elementDescriptors[containedType]

				if (containerFor) {
					elementDescriptors[elementDescriptorClass].containerFor = containerFor
				} else {
					throw new RivoliException("class '${containedType.simpleName}' can't be contained, since it lies outside the model")
				}
			}
		}
	}

	final static String PACKAGE_NAME = 'package name'
}